'''
Created on Jan 21, 2014

@author: aitang
'''

import sys
import re
import time
import pexpect
import os

class BusyLineError(Exception):
    """ If when we try to access the device console, and get EOF (typically
    when the line is taken), we raise BusyLineError."""
    pass

class CrashError(Exception):
    """ When certain strings are seen on the device console, for 
    example "Traceback", we raise this CrashError."""
    pass

class DeviceError(Exception):
    """ When we expect the device to be in a certain state, but it is not, 
    raise this DeviceError."""
    pass

class UserError(Exception):
    """ When the user violates the rule in the library, raise this UserError.
    For example, if calling exec_cmd but passing "config t" to it, this will
    change the state of the device and confuse the script.
    When this error is raised, it usually mean the user is doing something
    bad and should be using the library in a different way."""
    pass


class IosCli(object):
    ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
    
    def __init__(self, hostname, enable_password = "", log_file = None):

        """ Create the object of a Cisco device. Will be used as the base 
        class for IOS, ASA, etc.
        hostname is part of the prompt string for all Cisco devices. There
        are some variations such as ">", "#", "(config)#", depending on the
        actual mode of the device.
        """
        self.hostname = hostname
        self.disabled_prompt = hostname + '> '
        self.enabled_prompt = hostname + '# '
        self.config_prompt = hostname + '\([A-Za-z0-9-/]+\)# '
        self.enable_password = enable_password
        self.log_file = log_file


    def sendline(self, line):
        """ Send line to self.pe, followed by linefeed.
        This assumes that self.pe is created beforehand.
        """
        self.pe.sendline(line)
        
        
    def send(self, line):
        """ Send line to self.pe. This does not include 'return'.
        """
        self.pe.send(line)


    def expect_literally_no_LF(self, exp_string, timeout = 10):
        """ Given a string, try to see on self.pe if it is seen. 
        """
        
        res = self.pe.expect([pexpect.EOF, pexpect.TIMEOUT, 
                              re.escape(exp_string[:20])], timeout = timeout)
        
        if res == 0:
            raise BusyLineError
        elif res == 1:
            raise DeviceError("""Didn't see expected string. 
                Expecting: %s 
                Got: %s""" % (exp_string, self.pe.before))
        elif res == 2:
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
                

    def expect_LF(self, timeout = 10):

        res = self.pe.expect([pexpect.EOF, pexpect.TIMEOUT,
                             '\r\n'], timeout = timeout) 
        if res == 0:
            raise BusyLineError
        elif res == 1:
            raise DeviceError("""Didn't see LineFeed. 
                Got: %s""" % self.pe.before)
        elif res == 2:
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before


    def expect_literally(self, exp_string, timeout = 10):
        """ Given a string, try to see on self.pe if it is seen. 
            Also consumes the next LineFeed ('\r\n').
        """

        part1 = self.expect_literally_no_LF(exp_string, timeout)
        part2 = self.expect_LF()

        return part1 + part2

    def expect_regexp(self, exp_regexp, timeout = 10):
        """ Given a regexp pattern, try to see on self.pe if it is seen. 
        """
        
        res = self.pe.expect([pexpect.EOF, pexpect.TIMEOUT, 
                              exp_regexp], timeout = timeout)
        
        if res == 0:
            raise BusyLineError
        elif res == 1:
            raise DeviceError("""Didn't see expected regexp_pattern.
                Expecting: %s
                Got: %s""" % (exp_regexp, self.pe.before))
        elif res == 2:
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        
        
    def expect_prompt(self, timeout = 10):
        """ Expect the device prompt and set device state accordingly.
        """
        
        res = self.pe.expect([self.disabled_prompt, self.enabled_prompt, 
                              self.config_prompt, pexpect.TIMEOUT], 
                             timeout = 10)

        if res == 0:
            self.state = "DISABLED"
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        elif res == 1:
            self.state = "EXEC"
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        elif res == 2:
            self.state = "CONFIG"
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        else:
            raise DeviceError("expect_prompt timed out")
        
                
    def connect_console(self, term_server_ip, term_server_port):

        """ Connect to the Cisco CLI device's console port using the target 
        info. Also set up so that console output goes to the log_file for 
        debugging purposes."""

        # self.pe should not have been set. If it has, it means we are
        # redefining self.pe, and it is a UserError.
        try:
            self.pe
        except AttributeError:
            pass
        else:
            # a new instance should be used instead.
            raise UserError("self.pe redefinition is not allowed.")
            
        # Spawn console process.
        try:
            self.pe = pexpect.spawn("telnet %s %s" % (term_server_ip,
                term_server_port))
        except:
            raise DeviceError("spawn failed to telnet %s %s." % 
                              (term_server_ip, term_server_port)) 
        
        # Console output goes to individual files for each device.
        if self.log_file:
            fout = file(self.log_file, "a")
            self.pe.logfile = fout

        # The first thing after telnet is we see "Escape character is '^]'".
        self.expect_literally("Escape character is '^]'.", timeout = 3)


    def disconnect_console(self):

        # Send ctrl + ].
        self.send("\035")

        # You should see "telnet>".
        self.expect_literally("telnet>")

        # Send in "q". This should terminate the telnet.
        self.sendline("q")
        self.expect_literally("Connection closed.")
        

    def ssh_connect(self, mgmt_ip = None, username = None, password = None):

        """ SSH to the Cisco CLI device's ip port using the target
        info. Also set up so that port output goes to the log_file for
        debugging purposes."""

        # Spawn console process.
        try:
            self.pe = pexpect.spawn("ssh -l %s %s" % (self.username,
                                                       self.mgmt_ip))
        except:
            raise DeviceError("spawn failed to ssh %s." % mgmt_ip)

        # session output goes to log file.
        if self.log_file:
            fout = file(self.log_file, "a")
            self.pe.logfile = fout

        # The first thing after ssh is the prompt for password:
        self.expect_literally("Password:")

        # Then we send in the password:
        self.sendline(self.password)

        # Then we see the prompt:
        self.expect_literally(self.hostname)


    def ssh_disconnect(self):

        self.sendline('')
        self.expect_literally(self.hostname)
        self.sendline('exit')
        self.expect_regexp('Connection to \S+ closed.')


    def connect_mgmt_telnet(self, mgmt_ip, username = None, 
                            telnet_password = "cisco", enable_password = ""):

        """ Connect to the Cisco CLI device's management ip address by telnet.
        Also set up so that console output goes to the log_file for 
        debugging purposes."""

        try:
            self.pe
        except AttributeError:
            pass
        else:
            # a new instance should be used instead.
            raise UserError("self.pe redefinition is not allowed.")
            
        # Spawn console process.
        try:
            self.pe = pexpect.spawn("telnet %s" % mgmt_ip)
        except:
            raise DeviceError("spawn failed to telnet %s." % mgmt_ip) 
        
        # Console output goes to individual files for each device.
        if self.log_file:
            fout = file(self.log_file, "a")
            self.pe.logfile = fout

        # The first thing after telnet is we see "Escape character is '^]'".
        self.expect_literally("Escape character is '^]'.", timeout = 3)
        self.expect_literally("User Access Verification")
        
        res = self.pe.expect(["Username:", "Password:", pexpect.TIMEOUT], 
                             timeout = 10)
        
        if res == 0:
            assert username != None, "username not set in %s" % (
                sys._getframe().f_code.co_name)
            self.sendline(username)
            self.expect_literally(username)
            self.expect_literally("Password:")
            self.sendline(telnet_password)
            self.expect_literally(self.disabled_prompt) 
        elif res == 1:
            self.sendline(telnet_password)
            self.expect_literally(self.disabled_prompt) 
        else:
            raise DeviceError("""Didn't see expected string.
                Expecting either: "Username:" or "Password:",
                Got: %s""" % self.pe.before)
            
        self.sendline("enable")
        self.expect_literally("Password:") 
        self.sendline(enable_password)
        self.expect_literally(self.enabled_prompt) 
            

    def disconnect_mgmt_telnet(self):

        self.sendline("exit")
        self.expect_literally("Logoff")
        self.expect_literally("Connection closed by foreign host.")
        

    def _parse_prompt(self, trial_limit = 10):

        """ We try to call _parse_prompt_once up to trial_limit times. This
        is because the device may be in a weird state that prevents the first
        _parse_prompt_once to succeed. For example, if the device gives a
        <--- More ---> when the pager is set, _parse_prompt_once will get out
        of it but we need another _parse_prompt_once to do the job."""
        
        for _ in range(trial_limit):
            if self._parse_prompt_once():
                break 
        else:
            raise DeviceError("_parse_prompt failed after %d trials" %
                              trial_limit) 


    def _parse_prompt_once(self):

        """ We first send a "\r" to probe. We don't know what exactly to 
        expect, but it should be in one of the following states: 
        More prompt, DISABLED, EXEC, CONFIG. 
        
        About return value: if it is in one of the following three state:
        DISABLED, EXEC, CONFIG, return True. If More prompt is seen, turn
        pager off and return False. The caller can do _parse_prompt_once
        again. Otherwise, raise DeviceError.
        """

        # send "\r" and probe the state.
        self.send("\r")

        # more_prompt is when pager is set and output is too much.
        more_prompt = "<--- More --->"

        # We should see the prompt like this:
        # '\rasa5520-01/admin'
        res = self.pe.expect([more_prompt, self.disabled_prompt,
                              self.enabled_prompt, self.config_prompt,
                              pexpect.TIMEOUT], timeout = 3)

        if res == 0:
            self.sendline("q")
            return False
        if res == 1:
            self.state = "DISABLED"
            return True
        elif res == 2:
            self.state = "EXEC"
            return True
        elif res == 3:
            self.state = "CONFIG"
            return True
        else:
            raise DeviceError("_parse_prompt_once timed out")
        

    def init_prep(self):

        """ Probe what state we are in: DISABLED, EXEC or CONFIG.
        Then move to EXEC mode. 
        """

        self._parse_prompt()
        self._to_EXEC()
        self.exec_cmd("terminal length 0")
        self.show_version = self.exec_cmd("show version")


    def _to_EXEC(self):
        """ From all other states (DISABLED or CONFIG state), move to the 
        EXEC state. """

        if self.state == "DISABLED":
            self._DISABLED_to_EXEC()
        elif self.state == "EXEC":
            pass
        elif self.state == "CONFIG":
            self._CONFIG_to_EXEC()
            

    def _to_CONFIG_T_PROMPT(self):
        """ From all other states (DISABLED, EXEC, or deeply nested CONFIG
        state), move to the state where "config t" is entered. This is where
        all of our config_cmd() will be executed. """

        self._to_EXEC()
        self._EXEC_to_CONFIG()

            
    def _DISABLED_to_EXEC(self):
        """ Move from DISABLED state to EXEC mode. 
        """

        assert self.state == "DISABLED", "state not in DISABLED in %s" % (
            sys._getframe().f_code.co_name)
        
        self.sendline("enable")
        self.expect_literally("enable")
        
        self.expect_literally("Password:")

        # Send in password and re-parse the prompt.
        self.sendline(self.enable_password)
        
        self.expect_prompt()
        
        assert self.state == "EXEC", "state not in EXEC in %s" % (
            sys._getframe().f_code.co_name)
        

    def _EXEC_to_DISABLED(self):

        """ Move from EXEC state to DISABLED mode. """

        assert self.state == "EXEC", "state not in EXEC in %s" % (
            sys._getframe().f_code.co_name)
        
        self.sendline("disable")
        self.expect_literally("disable")
        
        self.expect_prompt()
        
        assert self.state == "DISABLED", "state not in DISABLED in %s" % (
            sys._getframe().f_code.co_name)
        
        
    def _EXEC_to_CONFIG(self):

        assert self.state == "EXEC", "state not in EXEC in %s" % (
            sys._getframe().f_code.co_name)
        
        """ Move from EXEC state to CONFIG mode. """

        self.sendline("config t")
        self.expect_literally("config t")
        
        self.expect_prompt()
        
        assert self.state == "CONFIG", "state not in CONFIG in %s" % (
            sys._getframe().f_code.co_name)
        
        
    def _CONFIG_to_EXEC(self):
        """ From CONFIG state, move to EXEC state. """

        assert self.state == "CONFIG", "state not in CONFIG in %s" % (
            sys._getframe().f_code.co_name)
        
        self.sendline("end")
        self.expect_literally("end")
        
        self.expect_prompt()
        
        assert self.state == "EXEC", "state not in EXEC in %s" % (
            sys._getframe().f_code.co_name)
        
        
    def exec_cmd(self, cmd, timeout = 15):

        """ A command is fed to the device, and the result is returned. """

        self._to_EXEC()

        # Make sure we don't see "disable", or "config t". Those should be
        # invoked by the config_cmd function.
        cmd = cmd.strip()
        if re.match("confi?g? +t", cmd) or re.match("disab?l?e?", cmd):
            raise UserError("Command not allowed in exec_cmd: %s" % cmd)
        
        # Now run the cmd and return the output.
        self.sendline(cmd)
        self.expect_literally(cmd, timeout)
        return_result = self.expect_prompt()

        # The output before the prompt is what we need to return.
        return return_result
        

    def config_cmd(self, cmd):

        """ a string of commands (separated by \n) is fed to the device. 
        The result is returned but only for the last line of cmd. Usually we
        don't care about the result -- just send the cmds over and we are 
        done. But if you need the return_result, it is your responsibility
        to call each cmd separately to get the return_result of each."""

        self._to_CONFIG_T_PROMPT()
        
        for line in cmd.split('\n'):
            line = line.strip()
            if len(line) == 0:
                continue # Empty line: skip
            self.sendline(line)
            self.expect_literally(line)
            return_result = self.expect_prompt()
            # In case a line of "end" is in the middle, we want to 
            # move back to CONFIG mode for the next line to succeed.
            if self.state == "EXEC":
                self._EXEC_to_CONFIG()

        self._to_EXEC()
        
        # The output before the prompt is what we need to return.
        return return_result
        

    def clear_config(self):
        
        """ run the command "write erase". """
        
        self._to_EXEC()
        
        self.sendline("write erase")
        self.expect_literally("[confirm]")
        self.sendline("")
        
        self.expect_literally("Erase of nvram: complete")
        self.expect_prompt()
        
        assert self.state == "EXEC", "state not in EXEC in %s" % (
            sys._getframe().f_code.co_name)
        
        
    def ping_is_good(self, ip):

        """ Try to ping an ip address. Returns True if 80% or higher success.
        Otherwise return False. """

        # Send the ping command.
        cmd = "ping " + ip
        return_result = self.exec_cmd(cmd, timeout = 15)

        # Then we check if ping is successful (> 80%).
        pat = re.compile("Success rate is ([0-9]+) percent")
        res = re.search(pat, return_result)

        assert res != None, "String not seen: Success rate is... in %s" % (
            sys._getframe().f_code.co_name)

        if int(res.group(1)) < 80:
            return False

        return True
    
        
        
        
class AsaCli(IosCli):
    ROBOT_LIBRARY_SCOPE = 'TEST SUITE'

    def __init__(self, hostname, enable_password = "", log_file = None):

        """ Create the class object of an ASA. """
        super(AsaCli, self).__init__(hostname, enable_password, log_file)
        
        
    def expect_literally_no_LF(self, exp_string, timeout = 10):
        """ Given a pattern to match into, try to see on self.pe if
        it is seen.
        If not, raise an exception. """
        
        res = self.pe.expect(["Traceback:", 
            "Configuration Replication is NOT performed from Standby unit"\
            " to Active unit.", pexpect.EOF, pexpect.TIMEOUT,
            re.escape(exp_string[:20])], timeout = timeout)
        
        if res == 0:
            raise CrashError('"Traceback:" is seen')
        elif res == 1:
            raise DeviceError("config change on standby not allowed.")
        elif res == 2:
            raise BusyLineError("Console line is busy")
        elif res == 3:
            raise DeviceError("""Didn't see expected string.
                Expecting: %s
                Got: %s""" % (exp_string, self.pe.before))
        elif res == 4:
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
                
                
    def expect_regexp(self, regexp_pattern, timeout = 10):
        """ Try to see on self.pe if regexp_pattern is seen. Return the
        output before prompt match.
        If not, raise an exception. """
        
        res = self.pe.expect(["Traceback:", pexpect.EOF, pexpect.TIMEOUT,
            regexp_pattern], timeout = timeout)
        
        if res == 0:
            raise CrashError('"Traceback:" is seen')
        # We don't do anything if an invalid command is used.
        # elif res == 1:
        #    pass
        elif res == 1:
            raise BusyLineError("Console line is busy")
        elif res == 2:
            raise DeviceError("""Didn't see expected regexp pattern.
                Expecting: %s
                Got: %s""" % (regexp_pattern, self.pe.before))
        elif res == 3:
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
    
                
    def expect_prompt(self, timeout = 10):
        """ Expect the device prompt and set device state accordingly.
        """
        
        res = self.pe.expect(["Traceback:", self.disabled_prompt, 
                              self.enabled_prompt, self.config_prompt, 
                              pexpect.TIMEOUT], timeout = 10)

        if res == 0:
            raise CrashError('"Traceback:" is seen')
        elif res == 1:
            self.state = "DISABLED"
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        elif res == 2:
            self.state = "EXEC"
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        elif res == 3:
            self.state = "CONFIG"
            sys.stdout.write(self.pe.before)
            sys.stdout.write(self.pe.after)
            return self.pe.before
        else:
            raise DeviceError("_parse_prompt_once timed out")
        
        
    def _parse_prompt_once(self):

        """ We first send a "\r" to probe. We don't know what exactly to 
        expect, but it should be in one of the following states: ROMMON, 
        DISABLED, EXEC, CONFIG. 
        
        About return value: if it times out, return False (so we can try this
        procedure again if we want). If other prompt is seen, return True.
        """

        # send "\r" and probe the state.
        self.send("\r")

        # more_prompt is when pager is set and output is too much.
        more_prompt = "<--- More --->"
        rommon_prompt = "\rrommon# [0-9]+>"
        smart_call_home_prompt = "Would you like to enable anonymous error"\
            " reporting"
        # remove_bad_commands is when you do some thing, such as "copy
        # disk0:/file running" and file contains cluster command that doesn't
        # like some inspect in the existing config. Need to answer "Y" so they
        # are removed.
        remove_bad_commands_prompt1 = "Would you like to remove these commands"
        remove_bad_commands_prompt2 = "Unexpected answer. Please try again."

        # We should see the prompt like this:
        # '\rasa5520-01/admin'
        res = self.pe.expect([more_prompt, rommon_prompt, self.disabled_prompt,
            self.enabled_prompt, self.config_prompt, smart_call_home_prompt, 
            remove_bad_commands_prompt1, remove_bad_commands_prompt2, 
            pexpect.TIMEOUT], timeout = 3)

        if res == 0:
            self.sendline("q")
            return False
        if res == 1:
            self.state = "ROMMON"
            raise DeviceError("In Rommon mode.")
        elif res == 2:
            self.state = "DISABLED"
            return True
        elif res == 3:
            self.state = "EXEC"
            return True
        elif res == 4:
            self.state = "CONFIG"
            return True
        elif res == 5:
            self.sendline("N")
            return False
        elif res == 6:
            self.sendline("Y")
            return False
        elif res == 7:
            self.sendline("Y")
            return False
        else:
            return False
        

    def init_prep(self):

        """ Probe what state we are in: DISABLED, EXEC or CONFIG.
        Then move to EXEC mode. 
        For ASA, there is multiple mode. If in multiple mode, move to 
        system space."""

        self._parse_prompt()
        self._to_EXEC()
        self.exec_cmd("terminal pager 0")
        self.show_version = self.exec_cmd("show version")
        self._check_hw()
        self._check_mode()
        if self.mode == "multiple":
            self.change_ctx("system")


    def exec_cmd(self, cmd, timeout = 15, ctx = None):

        """ A command is fed to asa, and the result is returned. If
        an ERROR is seen, we generate an error message and move on. """

        self._to_EXEC()
        if ctx != None:
            self.change_ctx(ctx)

        cmd = cmd.strip()
        
        # We assume the following commands aren't allowed in exec_cmd():
        # they are disallowed because they can potentially change the prompt
        # that will mess up with our script. 
        #
        # The following are simply disallowed. They should use other functions.
        # config t
        # disable
        # change context
        # failover

        # Make sure we don't see "conf t" or "config t". Those should be invoked
        # by the config_cmd function.
        if re.match("confi?g? +t", cmd) or re.match("disab?l?e?", cmd) or \
            re.match("changeto +.*", cmd) or re.match("failover +.*", cmd):
            raise UserError("Command not allowed in exec_cmd: %s" % cmd)
        
        # Now run the cmd and return the output.
        self.sendline(cmd)
        self.expect_literally(cmd, timeout)
        return_result = self.expect_prompt()

        if ctx != None:
            self.change_ctx("system")
            
        # The output before the prompt is what we need to return.
        return return_result
    

    def config_cmd(self, cmd, ctx = None):

        """ a string of commands (separated by \n) is fed to asa one by one.
        
        The result is returned but only for the last line of cmd. Usually we
        don't care about the result -- just send the cmds over and we are 
        done. But if you need the return_result, it is your responsibility
        to call each cmd separately to get the return_result of each."""

        self._to_CONFIG_T_PROMPT()
        
        if ctx != None:
            self.change_ctx("ctx")

        for line in cmd.split('\n'):
            line = line.strip()
            if len(line) == 0:
                continue # Empty line: skip
            self.sendline(line)
            self.expect_literally(line)
            return_result = self.expect_prompt()
            # In case a line of "end" is in the middle, we want to 
            # move back to CONFIG mode for the next line to succeed.
            if self.state == "EXEC":
                self._EXEC_to_CONFIG()

        self._to_EXEC()
        
        if ctx != None:
            self.change_ctx("system")

        # The output before the prompt is what we need to return.
        return return_result


    def _check_hw(self):
        """ From the output of "show version", parse out the hw model."""

        try:
            self.show_version
        except NameError:
            self.show_version = self.exec_cmd("show version", timeout = 15)

        res1 = re.search("Hardware:\s+ASA([\d-]+)", self.show_version)
        res2 = re.search("Hardware:\s+ASAv", self.show_version)

        if res1:
            self.hardware = res1.group(1)
        elif res2:
            self.hardware = "ASAv"
        else:
            raise DeviceError("""check_hw: Didn't see:"""\
                """ "Hardware:\s+ASA([\d-]+)" or "Hardware:\s+ASAv". """)
            

    def _check_mode(self):
        
        # 5505 we are in single mode:
        if self.hardware == "ASAv":
            self.mode = "single"
            return

        res = self.exec_cmd("show mode")

        pat = "Security context mode: ([\w]+)"
        m = re.search(pat, res)
        
        if m:
            if (m.group(1) == "multiple" or m.group(1) == "single"):
                self.mode = m.group(1)
            else:
                # unknown mode
                raise DeviceError("""check_mode mode is neither
                    multiple or single. """)
        else:
            raise DeviceError("check_mode: can not find mode.")

        # if in multiple mode, find all context names and put them in
        # self.ctx_name_list list.
        if self.mode == "multiple":
            self._ctx_name_list = ["system", ]
            self._check_ctx_name_list()    
            
        
    def _check_which_ctx(self):

        # Check what context is in use:
        if self.mode == "single":
            self.ctx = None
            return
        else:
            return_result = self.exec_cmd("show context")
            
            ### In system space, the output looks like this:
            # asa5520-01(config-ctx)# sh context
            # Context Name      Class      Interfaces           URL
            # *admin            default    Management0/0        disk0:/admin.cfg
            #  t                default    GigabitEthernet0/0,  disk0:/t.cfg
            #                               GigabitEthernet0/1  
            #
            # Total active Security Contexts: 2
            # asa5520-01/t(config)# 
            if re.search("Total active Security Contexts", return_result):
                self.ctx = "system"
                return
            else:
                ### In a context, the output looks like this:
                # asa5520-01(config-ctx)# sh context
                # Context Name      Class      Interfaces           URL
                #  t                default    GigabitEthernet0/0,  disk0:/t.cfg
                #                              GigabitEthernet0/1  
                # asa5520-01/t(config)# 
                marker_start = return_result.find("Context Name")
                next_line_start = return_result.find('\n', marker_start + 1)
                next_line_end = return_result.find('\n', next_line_start + 1)
                next_line = return_result[next_line_start + 1: next_line_end]
                
                pat = "^[\* ](\S+)\s+\S+\s+\S+\s+\S+"
                res = re.search(pat, next_line)

                self.ctx = res.group(1)


    def _check_ctx_name_list(self):
        
        if self.mode == "single":
            self.ctx_name_list = []
            return
        
        # we are in multiple mode.
        self.change_ctx("system")
        return_result = self.exec_cmd("show context")
        
        # first we see this line:
        # Context Name      Class      Interfaces           URL
        header_line = return_result.find("Context Name")
        assert header_line != -1, "String not seen: Context Name in %s" % (
            sys._getframe().f_code.co_name)
            
        start_of_ctx_name_list = return_result[header_line + 1:].find('\n')
        list_of_ctxs = return_result[start_of_ctx_name_list + 1:]
        
        # the output looks like this:
        # *admin            default    Management0/0        disk0:/admin.cfg
        #  t                default    Management0/1        disk0:/t.cfg
        for line in list_of_ctxs.split('\n'):
            r1 = re.match("[\*\s](\S+)\s+\S+\s+\S+\s+(\S+)\s*$", line)
            r2 = re.match("[\*\s](\S+)\s+\S+\s+\S+\s+\S+\s+(\S+)\s*$", line)
            if r1:
                self.ctx_name_list.append([r1.group(1), r1.group(2)])
            elif r2:
                self.ctx_name_list.append([r2.group(1), r2.group(2)])
            else:
                pass
            

    def check_fo_ready(self):
        """ Check whether this asa is in failover ready state, whether it is
        active, or standby ready. """

        # If in "multiple" mode, go to system space first.
        if self.mode == "multiple":
            self.change_ctx("system")

        show_fo_result = self.exec_cmd("show failover")

        # There are two kinds of failover output:
        #
        # 1. active/standby failover
        # This host: Primary - Active
        #  <snip>
        # Other host: Secondary - Standby Ready
        #
        # 2. active/active failover
        #  This host:    Primary
        #  Group 1       State:          Standby Ready
        #                Active time:    0 (sec)
        #  Group 2       State:          Standby Ready
        #                Active time:    0 (sec)
        #  <snip>
        #  Other host:   Secondary
        #  Group 1       State:          Active
        #                Active time:    807 (sec)
        #  Group 2       State:          Active
        #                Active time:    807 (sec)

        res = re.search("Failover On", show_fo_result)
        assert res != None, "String not seen: Failover On in %s" % (
            sys._getframe().f_code.co_name)
        
        ready_states = ["Active", "Standby Ready"]
        my_fo = re.search("This host: \w+ - ([^\r]+)\s+\r", show_fo_result)
        peer_fo = re.search("Other host: \w+ - ([^\r]+)\s+\r", show_fo_result)

        if my_fo and peer_fo:
            # active/standby fo

            if my_fo.group(1) in ready_states \
                and peer_fo.group(1) in ready_states:
                return True
            else:
                return False
        else:
            # active/active fo

            marker1 = show_fo_result.find("This host:")
            marker2 = show_fo_result.find("Other host:")
            
            assert marker1 != -1, "String not seen: This host: in %s" % (
                sys._getframe().f_code.co_name)
            assert marker2 != -1, "String not seen: Other host: in %s" % (
                sys._getframe().f_code.co_name)
            
            my_aa_fo = show_fo_result[marker1:marker2]
            peer_aa_fo = show_fo_result[marker2:]

            # active/active fo
            my_g1_fo = re.search("Group 1\s+State:\s+([^\r]+)\r", my_aa_fo)
            my_g2_fo = re.search("Group 2\s+State:\s+([^\r]+)\r", my_aa_fo)
            peer_g1_fo = re.search("Group 1\s+State:\s+([^\r]+)\r", peer_aa_fo)
            peer_g2_fo = re.search("Group 2\s+State:\s+([^\r]+)\r", peer_aa_fo)

            if my_g1_fo.group(1) in ready_states \
                and my_g2_fo.group(1) in ready_states \
                and peer_g1_fo.group(1) in ready_states \
                and peer_g2_fo.group(1) in ready_states:
                return True
            else:
                return False


    def change_ctx(self, ctx):
        
        """ change to the context. ctx can be "system", or name of the context,
        such as "t1". If "t1" is not a valid name, this error will be displayed.
        ERROR: 't1' is not valid 
        """

        assert self.mode == "multiple", "not multiple mode in %s" % (
            sys._getframe().f_code.co_name)
        
        assert ctx in self.ctx_name_list, "invalid ctx name in %s" % (
            sys._getframe().f_code.co_name)
        
        # We cannot call exec_cmd directly as this is disallowed there.
        cmd = "changeto context " + ctx
        self.sendline(cmd)

        # We should first see the command echoed.
        self.expect_literally(cmd)
        self.expect_prompt()
        
        self.ctx = ctx


    def ping_is_good(self, ip, ctx = None):

        """ Try to ping an ip address. Returns True if 80% or higher success.
        Otherwise return False. """

        # Send the ping command.
        cmd = "ping " + ip
        return_result = self.exec_cmd(cmd, timeout = 15, ctx = ctx)

        # Then we check if ping is successful (> 80%).
        pat = re.compile("Success rate is ([0-9]+) percent")
        res = re.search(pat, return_result)

        assert res != None, "String not seen: Success rate is... in %s" % (
                sys._getframe().f_code.co_name)

        if int(res.group(1)) < 80:
            return False

        return True
    
        

    def reload(self):

        """ We assume other steps are properly handled before this function
        is invoked: the image is copied into ASA's flash, and the 
        startup-config is properly saved. When we reload the device, it will 
        find the right image and config when it comes up. A good thing to do 
        is to configure "boot system <image>" line and "write memory", before 
        the reload.
        """

        # first, go to EXEC state.
        self._to_EXEC()

        # Send in the reload command.
        cmd = "reload noconfirm"
        self.sendline(cmd)

        # We should first see the reload command echoed.
        self.expect_literally(cmd)

        self._wait_for_reload()
        

    def _wait_for_reload(self):
        
        """ after the box is reloaded, we wait till it comes up and parse
        its prompt.
        Fixme: This does not handle a large startup-config parse time."""
        
        # we should see this line:
        # Cisco Systems, Inc.
        temp_prompt = "Cisco Systems, Inc"
        self.expect_literally(temp_prompt, 300)

        # Then we should see this line:
        # Type help or '?' for a list of available commands.
        temp_prompt = "Type help or '?' for a list of available commands."
        self.expect_literally(temp_prompt, 180)

        time.sleep(3)

        # Now probe prompt again as this will change.
        self._parse_prompt()


    def _reload_into_mode(self, to_mode):

        """ We assume the image is copied into ASA's disk0:/. Depending
        on whether we need a mode change, we either do a straight reload,
        or a mode change. """

        assert to_mode == "multiple" or to_mode == "single", "invalid mode"\
            " in %s" % (sys._getframe().f_code.co_name)

        # first, check current mode.
        self._check_mode()

        if self.mode == to_mode:
            self.reload()
            return

        # Now change mode.
        self._to_CONFIG_T_PROMPT()
        cmd = "mode noconfirm " + to_mode
        self.sendline(cmd)
        self.expect_literally(cmd)

        self._wait_for_reload()

        assert self.mode == to_mode, "in wrong mode after %s" % (
            sys._getframe().f_code.co_name)
        

    def _image_init_no_coredump(self, to_mode, mgmt_ifc, mgmt_name,
        mgmt_ip, mgmt_mask, tftp_server_ip, asa_image_name,
        tftp_dir_path = 'tftpboot'):
        
        """ Initialize ASA to the proper image and mode, with only
        basic running config.
        to_mode (string): single or multiple
        tftp_server_ip (string): ip address for tftp server.
        tftp_dir_path (string): path to do tftp file download.
        asa_image_name(string): name of image to download on asa.
        """
        
        # delete all disk0:/*.bin files
        self.exec_cmd("delete /noconfirm disk0:/*.bin")
        
        # clear all config and load the basic asa config
        self.clear_config_all()

        if self.mode == "multiple":
            cmd = """
                int %s
                 no shut
                admin admin
                context admin
                 allocate-interface %s
                 config-url disk0:/admin_init.cfg
                changeto context admin
                int %s
                 nameif %s
                 security 50
                 no shut
                """ % (mgmt_ifc, mgmt_ifc, mgmt_ifc, mgmt_name)
            cmd = cmd + "\nip address %s %s" % (mgmt_ip, mgmt_mask)
            self.config_cmd(cmd)
            self.exec_cmd("write memory all /noconfirm")
        else:
            cmd = """
                interface %s
                 nameif %s
                 no shut
                 security 50
                 """ % (mgmt_ifc, mgmt_name)
            cmd = cmd + "\nip address %s %s" % (mgmt_ip, mgmt_mask)
            self.config_cmd(cmd)
            self.exec_cmd("write memory")

        # tftp files in
        # the first one should be asa os image
        tftp_location = os.path.join(tftp_dir_path, asa_image_name)
        self.tftp_file_in(tftp_server_ip, tftp_location, 
            local_location = "disk0:", timeout = 120)
        
        # reload into the proper mode
        self.config_cmd("boot system disk0:/%s" % asa_image_name)
        self.exec_cmd("write memory")
        self._reload_into_mode(to_mode)


    def _coredump_enable(self, location = "disk0:", size = 500):
        
        """ enable coredump. """
        
        self._to_CONFIG_T_PROMPT()
        
        self.sendline("coredump enable filesystem %s size %d" % (location, 
                                                                    size))
        self.expect_literally("[confirm]")
        
        self.sendline("")
        exp_string = "Coredump file system image created & mounted successfully"
        self.expect_literally(exp_string, 180)
        
        
    def image_init(self, to_mode, mgmt_ifc, mgmt_name, mgmt_ip, mgmt_mask,
        tftp_server_ip, asa_image_name, tftp_dir_path = 'tftpboot'):

        self._image_init_no_coredump(to_mode, mgmt_ip, mgmt_mask, 
            tftp_server_ip, tftp_dir_path, asa_image_name)
        self._coredump_enable()
        

    def _clear_config_secondary(self):
        """ We run "clear config secondary" on ASA. This clears hostname
            as well so then we add back hostname.
            This way, all the interface config stay un-changed, to
            avoid the delay of many seconds to bring up an interface
            after the "clear config all" command.
        """

        # First, go to EXEC state. If multiple mode, go to system space.
        self._to_EXEC()
        if self.mode == "multiple" and self.ctx != "system":
            self.change_ctx("system")

        # "clear shun" is not automatically invoked.
        self.exec_cmd("clear shun")

        self._EXEC_to_CONFIG()
        self.sendline("clear config secondary")
        self.expect_literally("clear config secondary")
        self.expect_literally("#", timeout = 30)
        
        self.sendline("hostname %s" % self.hostname)
        self.expect_literally("hostname %s" % self.hostname)
        self._parse_prompt()
        
        self._to_EXEC()


    def clear_config_all(self):
        """ We run "clear config all" on ASA, then add back hostname."""

        # First, go to EXEC state. If multiple mode, go to system space.
        self._to_EXEC()
        if self.mode == "multiple" and self.ctx != "system":
            self.change_ctx("system")
        
        self.exec_cmd("clear shun")

        self._EXEC_to_CONFIG()
        self.sendline("clear config all")
        self.expect_literally("clear config all")
        
        self.expect_literally("#", timeout = 30)
        
        self.sendline("hostname %s" % self.hostname)
        self.expect_literally("hostname %s" % self.hostname)
        self._parse_prompt()
        
        self._to_EXEC()


    def tftp_file_in(self, tftp_server_ip, tftp_location,
            local_location = "disk0:", timeout = 10):

        """ We assume the following:
        -- image is stored on the server, under the right path.
        -- ASA has good network connectivity to tftp server."""

        timeout = int(timeout)
        
        # First, go to EXEC state. If multiple mode, go to system space.
        self._to_EXEC()
        if self.mode == "multiple" and self.ctx == "system":
            self.change_ctx("system")

        assert self.ping_is_good(tftp_server_ip), "ping failure in %s" % (
            sys._getframe().f_code.co_name)

        # Send in the tftp command.
        cmd = "copy /noconfirm tftp://%s/%s %s" % (tftp_server_ip, 
            tftp_location, local_location) 
        
        self.sendline(cmd)
        self.expect_literally(cmd[:20])
        self.expect_literally_no_LF("Accessing tftp://")
        self.expect_literally_no_LF("!")

        # after copy we should see a line like this:
        # Writing file disk0:/asa100-8-38-48-smp-k8.bin...
        # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        for _ in range(timeout):
            res = self.pe.expect(["Writing file", pexpect.TIMEOUT], timeout = 1)
            if res == 0:
                break
            else:
                assert "!" in self.pe.before, "not seeing ! in %s" % (
                    sys._getframe().f_code.co_name)
                time.sleep(0.1)
        else:
            raise DeviceError("Writing file not seen after %d" % timeout)

        for _ in range(timeout):
            res = self.pe.expect(["[0-9]+ bytes copied in [0-9\.]+ secs",
                pexpect.TIMEOUT], timeout = 1)
            if res == 0:
                break
            else:
                assert "!" in self.pe.before, "not seeing ! in %s" % (
                    sys._getframe().f_code.co_name)
                time.sleep(0.1)
        else:
            raise DeviceError("[0-9]+ bytes copied in [0-9\.]+ secs not seen"\
                " after %d" % timeout)
        

    def tftp_file_out(self, local_file, tftp_server_ip, tftp_location, 
        timeout = 10):

        """ If filename is not specified (set to None), we use:
        -- for single mode, "%s.cfg" % self.hostname
        -- for multiple mode, "%s_%s.cfg" % (self.hostname, ctx_name) where
        ctx_name is either system, or context name. """

        # First, go to EXEC state. If multiple mode, go to system space.
        self._to_EXEC()
        if self.mode == "multiple" and self.ctx != "system":
            self.change_ctx("system")

        assert self.ping_is_good(tftp_server_ip, limit = 60), "ping failed"\
            " in %s" % (sys._getframe().f_code.co_name)

        cmd = "copy /noconfirm %s tftp://%s/%s" % (local_file,
                tftp_server_ip, tftp_location)
            
        res = self.exec_cmd(cmd, timeout = timeout)
        r = re.search("[0-9]+ bytes copied in [0-9\.]+ secs", res)
        
        assert r != None, "String not seen: bytes copied in... in %s" % (
            sys._getframe().f_code.co_name)

